package com.googlecode.alogwe.sprockit.js;

import java.io.FileNotFoundException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.commonjs.module.Require;

import com.googlecode.alogwe.sprockit.engine.JSEngine;

public class JSScript extends ScriptableObject implements Runnable {

	/**
	 * The zero-parameter constructor, required by Rhino for all host objects.
	 * <p>
	 * When Context.defineClass is called with this class, it will construct
	 * Script.prototype using this constructor.
	 */
	public JSScript() {
	}

	/**
	 * This is the main constructor, marked private and only accessible through
	 * the {@link JSScript#createNew(String, String)} method.
	 * <p>
	 * When Context.defineClass is called with this class, it will construct
	 * Script.prototype using this constructor.
	 * 
	 * @throws FileNotFoundException
	 */
	private JSScript(String filePath) throws FileNotFoundException {
		this.file = new JSFile(filePath);
	}

	/**
	 * The Java method defining the JavaScript constructor.
	 */
	public void jsConstructor() {
	}

	private static final String	JS_CLASSNAME	= "Script";

	/**
	 * Returns the name of this JavaScript class. Typically the same name as the
	 * constructor.
	 */
	@Override
	public String getClassName() {
		return JS_CLASSNAME;
	}

	/**
	 * Executes a JavaScript function in this script.
	 * <p>
	 * <b>TODO</b> Have this return result from JavaScript;
	 * 
	 * @param cx
	 *            JavaScript Context to work with
	 * @param scope
	 *            JavaScript runtime scope (object) to look for the specified
	 *            function name
	 * @param functionName
	 *            The name of the script function to call
	 * @throws Exception
	 */
	private void callJsFunction(Context cx, Scriptable scope, String functionName) throws Exception {

		Object jsFunction = scope.get(functionName, scope);

		if (!(jsFunction instanceof Function)) {
			throw new Exception(functionName
					+ "is not a valid or callable Function.");
		}
		Function f = (Function) jsFunction;
		Object mainArgs[] = {};
		Object result = f.call(cx, scope, scope, mainArgs);
		String report = Context.toString(result);
		System.out.println(report);
		// return result; // TODO;
	}

	/**
	 * Creates it's own JavaScript host object in the given context/scope
	 * <p>
	 * <b>TODO</b> Figure out a better way to do this.
	 * 
	 * @param cx
	 *            The Context used to create the JavaScript host object
	 * @param scope
	 *            The object in the JavaScript environment that will receive
	 *            this newly created host object
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws InvocationTargetException
	 */
	public static void createJsHostObject(Context cx, Scriptable scope)
			throws IllegalAccessException, InstantiationException, InvocationTargetException {
		ScriptableObject.defineClass(scope, JSScript.class);
	}

	/**
	 * Creates a new JSScript instance.
	 * 
	 * @param filepath
	 *            The relative local path to the JavaScript script file.<br>
	 * @param entryFunction
	 *            The name of the main JavaScript function which will
	 *            automatically be called upon loading.
	 * @return A new JSScript instance
	 * @throws FileNotFoundException
	 */
	public static JSScript createNew(String filepath, String entryFunction)
			throws FileNotFoundException {
		JSScript script = new JSScript(filepath);
		script.setEntryFunction(entryFunction);
		return script;
	}

	/*
	 * Context instance
	 */
	private Context	cx;

	public Context getContext() {
		return this.cx;
	}

	/*
	 * JSFile instance
	 */
	private JSFile	file;

	public JSFile getFile() {
		return this.file;
	}

	/*
	 * JavaScript entry function (Main script function)
	 */
	private String	jsEntryFunction;

	public String getEntryFunction() {
		return this.jsEntryFunction;
	}

	public void setEntryFunction(String functionName) {
		this.jsEntryFunction = functionName;
	}

	/*
	 * Scope instance
	 */
	private Scriptable	topLevelScope;

	public Scriptable getScope() {
		return this.topLevelScope;
	}

	// TODO Extend ContextFactory and use that for more/better feature
	// customization;
	// TODO Properly handle threads/exceptions/exiting the Context;
	@Override
	public void run() {
		try {

			/*
			 * Initialize this thread Context, and all of JavaScript's standard
			 * language features
			 */
			cx = Context.enter();
			cx.setLanguageVersion(Context.VERSION_1_8);
			topLevelScope = cx.initStandardObjects();

			/*
			 * Install CommonJS modules/require() features
			 */
			Require require = JSEngine.createJsRequire(cx, topLevelScope);
			require.install(topLevelScope);

			/*
			 * Create JavaScript Host Object from each class that provides
			 * scripting API
			 */
			JSConsole.createJsHostObject(cx, topLevelScope);
			JSEngine.createJsHostObject(cx, topLevelScope);
			JSFile.createJsHostObject(cx, topLevelScope);
			JSProcess.createJsHostObject(cx, topLevelScope);
			JSScript.createJsHostObject(cx, topLevelScope);
			JSSystem.createJsHostObject(cx, topLevelScope);

			/*
			 * Read the JavaScript source from the file which this JSScript
			 * represents. Then execute it against this Thread Context and Scope
			 * (the JavaScript environment's global Object)
			 */
			String sourceName = getFile().getPath();
			int startLine = 0;

			String source = "";
			ArrayList<String> arr = getFile().read();
			for (String s : arr) {
				source += s;
			}

			// TODO Possibly use the result;
			Script script = cx.compileString(source, sourceName, startLine, null);
			Object result = script.exec(cx, topLevelScope);

			/*
			 * Pass execution to the script's entry function. Unless otherwise
			 * specified, the main() function is called.
			 */
			callJsFunction(cx, topLevelScope, jsEntryFunction);

		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			Context.exit();
		}
	}

	@Override
	public String toString() {
		Thread t = Thread.currentThread();
		String filename = this.getFile().getPath();
		return "JSScript [\"" + filename + "\"] [Thread:" + t.getId() + "]";
	}

}